<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>爱心雨 — 可编辑并分享的单文件页面</title>
  <style>
    :root{--bg:#070717}
    html,body{height:100%;margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,'PingFang SC','Hiragino Sans GB',"Microsoft YaHei",sans-serif;background:var(--bg);color:#fff}
    .app{height:100%;display:flex;flex-direction:column}
    header{display:flex;gap:12px;align-items:center;padding:10px 14px;background:linear-gradient(90deg, rgba(255,255,255,0.02), transparent);backdrop-filter: blur(4px)}
    .brand{font-weight:700;font-size:16px}
    .controls{margin-left:auto;display:flex;gap:8px;align-items:center}
    input[type=text]{padding:6px 8px;border-radius:6px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.02);color:#fff;min-width:200px}
    button{padding:7px 10px;border-radius:8px;border:0;background:#ff6b9a;color:#fff;cursor:pointer}
    button.ghost{background:transparent;border:1px solid rgba(255,255,255,0.08)}
    main{flex:1;position:relative;overflow:hidden}
    canvas{display:block;width:100%;height:100%}
    .hint{position:absolute;left:12px;bottom:12px;padding:8px 10px;border-radius:8px;background:rgba(0,0,0,0.35);font-size:13px}
    .toast{position:fixed;right:12px;top:12px;padding:8px 12px;border-radius:8px;background:rgba(0,0,0,0.6)}
  </style>
</head>
<body>
  <div class="app">
    <header>
      <div class="brand">爱心雨（单文件）</div>
      <div class="controls">
        <label for="nameInput">显示名字：</label>
        <input id="nameInput" type="text" value="你的名字" />
        <button id="applyBtn">应用</button>
        <button id="shareBtn">发布 / 复制分享链接</button>
        <button id="downloadBtn" class="ghost">下载 HTML</button>
      </div>
    </header>
    <main>
      <canvas id="c"></canvas>
      <div class="hint">移动鼠标可产生尾迹；输入名字后点“应用”，再点“发布”复制可分享的 URL。</div>
    </main>
  </div>

  <script>
  // Single-file LOVE RAIN converted from Python/PyGame to HTML5 Canvas + JS
  // Features:
  // - 爱心雨粒子（落下并被吸附到文字心形）
  // - 鼠标尾迹小心心
  // - 可编辑名字，应用后重新生成文字心形
  // - 发布按钮会在 URL hash 中写入编码后的状态，便于分享（无需服务器）

  (() => {
    const canvas = document.getElementById('c');
    const ctx = canvas.getContext('2d', { alpha: true });
    let W = 800, H = 600;
    function resize(){
      const dpr = window.devicePixelRatio || 1;
      W = canvas.clientWidth = canvas.offsetWidth || Math.max(600, window.innerWidth);
      H = canvas.clientHeight = canvas.offsetHeight = Math.max(400, window.innerHeight - 64);
      canvas.width = Math.round(W * dpr);
      canvas.height = Math.round(H * dpr);
      ctx.setTransform(dpr,0,0,dpr,0,0);
    }
    window.addEventListener('resize', resize);
    resize();

    // Parameters
    const NUM_RAINDROPS = 260;
    const NUM_MOUSE_HEARTS = 6;
    let TEXT = '你的名字';

    // Heart parametric function (port of python version)
    function heartFunction(t, scale=12){
      const x = scale * 16 * Math.pow(Math.sin(t),3);
      const y = -scale * (13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t));
      return [x,y];
    }

    // Utility: draw a small heart made of dots with glow
    function drawHeartGradient(x0, y0, size, color, alphaBase=0.85){
      // color = [r,g,b]
      // draw glow using radial gradient
      const glowRadius = size * 2.4;
      const g = ctx.createRadialGradient(x0, y0, Math.max(4, size*0.2), x0, y0, glowRadius);
      g.addColorStop(0, `rgba(${color[0]},${color[1]},${color[2]},${alphaBase*0.9})`);
      g.addColorStop(0.6, `rgba(${color[0]},${color[1]},${color[2]},${alphaBase*0.35})`);
      g.addColorStop(1, `rgba(${color[0]},${color[1]},${color[2]},0)`);
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(x0, y0, glowRadius, 0, Math.PI*2);
      ctx.fill();

      // draw heart outline by plotting many small points along param
      const steps = Math.max(24, Math.round(size*3.5));
      ctx.save();
      ctx.globalCompositeOperation = 'lighter';
      for(let i=0;i<steps;i++){
        const t = (Math.PI*2) * (i/steps);
        const [hx,hy] = heartFunction(t, size/20);
        const px = x0 + hx;
        const py = y0 + hy;
        const dotSize = Math.max(0.8, size/12 + (Math.random()-0.5)*0.6);
        ctx.beginPath();
        ctx.fillStyle = `rgba(${color[0]},${color[1]},${color[2]},${alphaBase})`;
        ctx.arc(px, py, dotSize, 0, Math.PI*2);
        ctx.fill();
      }
      ctx.restore();
    }

    // Particle classes
    class Raindrop{
      constructor(){ this.reset(); }
      reset(){
        this.x = Math.random()*W;
        this.y = Math.random()*-H;
        this.speed = 6 + Math.random()*4;
        this.max_speed = 20 + Math.random()*15;
        this.acceleration = 0.5 + Math.random()*0.5;
        this.decelerating = false;
        this.captured = false;
        this.tx = this.ty = this.tz = null;
        this.fall_time = 0;
        this.size = 6 + Math.floor(Math.random()*8);
      }
      fall(){
        if(!this.captured){
          if(!this.decelerating){
            this.speed += this.acceleration;
            if(this.speed>=this.max_speed){ this.speed = this.max_speed; this.decelerating = true; }
          } else {
            this.speed *= 0.92;
            if(this.speed < 0.5) this.speed = 0.0;
          }
          this.y += this.speed;
          this.fall_time += 1;
          if(this.speed === 0.0 && this.fall_time > 30){
            const t = Math.random()*Math.PI*2;
            const [hx,hy] = heartFunction(t, 18);
            this.tx = W/2 + hx;
            this.ty = H/2 + hy;
            this.tz = -80 + Math.floor(Math.random()*160);
            this.captured = true;
          }
          if(this.y > H + 80) this.reset();
        } else {
          this.x += (this.tx - this.x) * 0.3;
          this.y += (this.ty - this.y) * 0.3;
        }
      }
      draw(){
        if(!this.captured){
          const ratio = (this.y % 100) / 100;
          const r = Math.floor(180 + (255-180)*ratio);
          const g = Math.floor(200 + (180-200)*ratio);
          const b = Math.floor(255 + (220-255)*ratio);
          drawHeartGradient(this.x, this.y, this.size, [r,g,b], 0.6 + Math.random()*0.4);
        } else {
          const scale = 200 / (200 + this.tz);
          let size = Math.max(4, Math.floor(this.size * scale));
          const pulse = 1 + 0.05 * Math.sin(perf.now() / 400);
          size = Math.floor(size * pulse);
          const color = (this.tz < 0) ? [220,190,255] : [255,180,220];
          drawHeartGradient(this.x, this.y, size, color, 0.7);
        }
      }
    }

    class MouseHeart{
      constructor(x,y,vx,vy){
        this.x = x; this.y = y; this.vx = vx + (Math.random()*0.6-0.3); this.vy = vy + (Math.random()*0.6-0.3);
        this.size = 2 + Math.floor(Math.random()*4);
        this.color = [180 + Math.floor(Math.random()*75), 180 + Math.floor(Math.random()*75), 220 + Math.floor(Math.random()*35)];
        this.life = 40;
      }
      update(){ this.x += this.vx; this.y += this.vy; this.vy += 0.05; this.life -= 1; }
      draw(){ drawHeartGradient(this.x, this.y, this.size, this.color, Math.max(0.25, this.life/40)); }
    }

    class TextParticle{
      constructor(tx,ty){
        this.tx = tx; this.ty = ty;
        this.x = Math.random()*W; this.y = -Math.random()*H;
        this.size = 1 + Math.random()*2;
        this.color = [180 + Math.floor(Math.random()*75), 180 + Math.floor(Math.random()*75), 220 + Math.floor(Math.random()*35)];
        this.start_x = this.x; this.start_y = this.y;
        this.start_time = performance.now()/1000;
        this.duration = 3; this.captured = false;
      }
      update(){
        const current = performance.now()/1000;
        let t = (current - this.start_time)/this.duration;
        if(t >= 1){ t = 1; this.captured = true; }
        this.x = this.start_x + (this.tx - this.start_x)*t;
        this.y = this.start_y + (this.ty - this.start_y)*t;
      }
      draw(){ drawHeartGradient(this.x, this.y, 1.5 + this.size, this.color, 0.9); }
    }

    // Create initial raindrops
    const raindrops = Array.from({length: NUM_RAINDROPS}, ()=>new Raindrop());
    const heartDrops = [];
    let mouseHearts = [];
    let textParticles = [];

    // Generate positions for text heart by rendering to offscreen canvas and sampling alpha
    function getTextHeartPositions(text){
      const off = document.createElement('canvas');
      const s = Math.min(160, Math.max(80, Math.floor(Math.min(W,H)/4)));
      off.width = s * 6; off.height = s * 2.2;
      const g = off.getContext('2d');
      g.clearRect(0,0,off.width,off.height);
      const fontSize = Math.floor(s * 0.9);
      g.font = `bold ${fontSize}px "SimHei", "Microsoft YaHei", sans-serif`;
      g.textAlign = 'center'; g.textBaseline='middle';
      g.fillStyle = '#fff';
      g.fillText(text, off.width/2, off.height/2 + fontSize*0.05);
      const img = g.getImageData(0,0,off.width,off.height);
      const positions = [];
      for(let y=0;y<off.height;y+=3){
        for(let x=0;x<off.width;x+=3){
          const idx = (y*off.width + x) * 4 + 3; // alpha
          if(img.data[idx] > 50){
            const px = (W/2 - Math.floor(off.width/2)) + x + (Math.random()*2-1);
            const py = (H/2 - Math.floor(off.height/2)) + y + (Math.random()*2-1);
            positions.push([px,py]);
          }
        }
      }
      return positions;
    }

    function rebuildTextParticles(){
      const pos = getTextHeartPositions(TEXT);
      textParticles = pos.map(([px,py])=>new TextParticle(px,py));
    }

    rebuildTextParticles();

    // Mouse tracking
    let lastMouse = {x: W/2, y: H/2};
    window.addEventListener('mousemove', (ev)=>{
      const r = canvas.getBoundingClientRect();
      const mx = ev.clientX - r.left; const my = ev.clientY - r.top;
      const dx = mx - lastMouse.x; const dy = my - lastMouse.y;
      if((dx!==0 || dy!==0)){
        for(let i=0;i<NUM_MOUSE_HEARTS;i++){
          const speed_factor = 0.2;
          const vx = dx * speed_factor; const vy = dy * speed_factor;
          mouseHearts.push(new MouseHeart(mx,my,vx,vy));
          // spread +/-30 degree
          const angle = 30 * Math.PI/180;
          const cosA = Math.cos(angle), sinA = Math.sin(angle);
          mouseHearts.push(new MouseHeart(mx,my, vx * cosA - vy * sinA, vx * sinA + vy * cosA));
          mouseHearts.push(new MouseHeart(mx,my, vx * cosA + vy * sinA, -vx * sinA + vy * cosA));
        }
      }
      lastMouse = {x: mx, y: my};
    });

    // Animation loop
    function frame(){
      ctx.clearRect(0,0,W,H);
      // dark background with subtle gradient
      const bg = ctx.createLinearGradient(0,0,0,H);
      bg.addColorStop(0, '#070717'); bg.addColorStop(1, '#0b0b1a');
      ctx.fillStyle = bg; ctx.fillRect(0,0,W,H);

      // update raindrops
      for(const drop of raindrops){ drop.fall(); if(drop.captured && !heartDrops.includes(drop)) heartDrops.push(drop); }

      // update text particles
      for(const tp of textParticles){ tp.update(); tp.draw(); }

      // update mouse hearts
      for(let i = mouseHearts.length-1; i>=0; i--){
        const mh = mouseHearts[i]; mh.update(); if(mh.life<=0) mouseHearts.splice(i,1);
      }

      // draw raindrops
      for(const drop of raindrops){ drop.draw(); }

      // sort captured hearts by tz and draw
      heartDrops.sort((a,b)=> (b.tz||0) - (a.tz||0));
      for(const d of heartDrops) d.draw();

      // draw mouse hearts
      for(const mh of mouseHearts) mh.draw();

      requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);

    // Controls: inputs and share / download
    const nameInput = document.getElementById('nameInput');
    const applyBtn = document.getElementById('applyBtn');
    const shareBtn = document.getElementById('shareBtn');
    const downloadBtn = document.getElementById('downloadBtn');

    applyBtn.addEventListener('click', ()=>{
      TEXT = (nameInput.value || ' ' ).slice(0, 40);
      rebuildTextParticles();
      showToast('已应用名字: ' + TEXT);
      updateHashState();
    });

    function getState(){ return { name: TEXT }; }
    function setState(state){ if(state && state.name){ TEXT = state.name; nameInput.value = TEXT; rebuildTextParticles(); } }

    function encodeState(state){ try{return btoa(unescape(encodeURIComponent(JSON.stringify(state)))); }catch(e){return ''; } }
    function decodeState(s){ try{ const str = decodeURIComponent(escape(atob(s))); return JSON.parse(str); }catch(e){return null; } }

    function updateHashState(){ const enc = encodeState(getState()); if(enc) location.hash = 's=' + enc; }

    // On load parse hash
    (function(){
      if(location.hash && location.hash.startsWith('#s=')){
        const enc = location.hash.slice(3);
        const st = decodeState(enc);
        if(st) setState(st);
      }
    })();

    shareBtn.addEventListener('click', ()=>{
      updateHashState();
      try{
        navigator.clipboard.writeText(location.href).then(()=> showToast('已复制分享链接到剪贴板')); 
      }catch(e){
        // fallback
        const tmp = document.createElement('textarea'); tmp.value = location.href; document.body.appendChild(tmp); tmp.select(); document.execCommand('copy'); tmp.remove(); showToast('已复制分享链接到剪贴板');
      }
    });

    // Download single-file HTML (re-embed current state into file)
    downloadBtn.addEventListener('click', ()=>{
      updateHashState();
      const html = `<!doctype html>\n${document.documentElement.outerHTML}`;
      const blob = new Blob([html], {type:'text/html'});
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a'); a.href = url; a.download = 'love_rain_shared.html'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
      showToast('已生成并下载单文件 HTML');
    });

    // small toast
    let toastT = null;
    function showToast(text){
      if(toastT){ clearTimeout(toastT); }
      let el = document.querySelector('.toast');
      if(!el){ el = document.createElement('div'); el.className = 'toast'; document.body.appendChild(el); }
      el.textContent = text; el.style.opacity = '1';
      toastT = setTimeout(()=>{ if(el) el.style.opacity = '0'; }, 2500);
    }

    // initial state in hash
    updateHashState();

  })();
  </script>
</body>
</html>
